<?php


namespace EchoPHP\Http;


use EchoPHP\Foundation\Support\FileBag;
use EchoPHP\Foundation\Support\HeaderBag;
use EchoPHP\Foundation\Support\ParameterBag;
use EchoPHP\Foundation\Support\ServerBag;
use EchoPHP\Http\Support\InteractsWithInput;
use EchoPHP\Support\Str;

/**
 * http request请求类
 * todo - 目前如charset等信息解析尚未处理
 * @package EchoPHP\Http
 */
class Request {

    use InteractsWithInput;

    const HEADER_X_NS_CLIENTPORT = 0b000001;
    const HEADER_X_NS_CLIENTIP = 0b000010;
    const HEADER_X_FORWARDED_FOR = 0b000100;
    const HEADER_X_FORWARDED_HOST = 0b001000;
    const HEADER_X_FORWARDED_PROTO = 0b010000;
    const HEADER_X_FORWARDED_PORT = 0b100000;


    /**
     * Query String $_GET 参数
     * @var \EchoPHP\Foundation\Support\ParameterBag
     */
    protected $query;

    /**
     * request body $_POST 参数
     * @var \EchoPHP\Foundation\Support\ParameterBag
     */
    protected $request;

    /**
     * request attribute PATH_INFO 中解析出来的自定义参数
     * @var \EchoPHP\Foundation\Support\ParameterBag
     */
    protected $attributes;

    /**
     * $_COOKIE 参数
     * @var \EchoPHP\Foundation\Support\ParameterBag
     */
    protected $cookies;

    /**
     * 表单提交的文件 $_FILES
     * @var \EchoPHP\Foundation\Support\FileBag
     */
    protected $files;

    /**
     * 服务器信息及环境变量 $_SERVER
     * @var \EchoPHP\Foundation\Support\ServerBag
     */
    protected $server;

    /**
     * http请求头信息
     * @var \EchoPHP\Foundation\Support\HeaderBag
     */
    protected $headers;

    /**
     * raw body data 原始表单字符串
     * @var string|null
     */
    protected $content;

    /**
     * url中pathInfo部分
     * @var string
     */
    protected $pathInfo;

    /**
     * 请求方式
     * @var
     */
    protected $method;

    protected $domain;

    protected $host;

    protected $port;

    protected $url;

    protected $baseUrl;

    protected $baseUrlWithoutPath;

    protected $queryString;

    /**
     * 当前请求的文档格式
     * @var
     */
    protected $format;

    /**
     * 文档格式集合
     * @var array
     */
    protected $formats = [
        'html' => ['text/html', 'application/xhtml+xml'],
        'txt' => ['text/plain'],
        'js' => ['application/javascript', 'application/x-javascript', 'text/javascript'],
        'css' => ['text/css'],
        'json' => ['application/json', 'application/x-json'],
        'xml' => ['text/xml', 'application/xml', 'application/x-xml'],
        'rdf' => ['application/rdf+xml'],
        'atom' => ['application/atom+xml'],
        'rss' => ['application/rss+xml'],
        'form' => ['application/x-www-form-urlencoded'],
    ];

    /**
     * 受信任的代理服务器IP
     * @var array
     */
    protected $trustedProxies = [];

    /**
     * 代理服务器转发时真实客户IP的header名称
     * @var array
     */
    private static $trustedHeaders = [
        self::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
        self::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
        self::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
        self::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
        self::HEADER_X_NS_CLIENTIP => 'NS_CLIENTIP',
        self::HEADER_X_NS_CLIENTPORT => 'NS_CLIENTPORT',
    ];

    protected static $requestFactory;

    /**
     * 获取请求实例
     * @return $this
     */
    public function instance() {
        return $this;
    }

    public function query() {
        return $this->query;
    }

    public function request() {
        return $this->request;
    }

    public function attribute() {
        return $this->attributes;
    }

    public function cookie() {
        return $this->cookies;
    }

    public function file() {
        return $this->files;
    }

    public function server() {
        return $this->server;
    }

    public function header() {
        return $this->headers;
    }

    /**
     * request类构造函数
     * @param array $query GET 参数
     * @param array $request POST 参数
     * @param array $attributes REQUEST attribute参数 即PATH_INFO 中解析出来的参数
     * @param array $cookies $_COOKIE 参数
     * @param array $files $_FILES 参数
     * @param array $server $_SERVER 参数
     * @param null $content raw body data 原始表单提交字符串
     */
    public function __construct(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null) {
        $this->initialize($query, $request, $attributes, $cookies, $files, $server, $content);
    }

    /**
     * 克隆一个当前request请求
     */
    public function __clone() {
        $this->query = clone $this->query;
        $this->request = clone $this->request;
        $this->attributes = clone $this->attributes;
        $this->cookies = clone $this->cookies;
        $this->files = clone $this->files;
        $this->server = clone $this->server;
        $this->headers = clone $this->headers;
    }

    /**
     * 返回请求类的字符串格式
     * @return bool|string
     */
    public function __toString() {
        try {
            $content = $this->getContent();
        } catch (\LogicException $e) {
            return trigger_error($e, E_USER_ERROR);
        }

        $cookieHeader = '';
        $cookies = [];
        foreach ($this->cookies as $k => $v) {
            $cookies[] = $k . '=' . $v;
        }
        if (!empty($cookies)) {
            $cookieHeader = 'Cookie: ' . implode(';', $cookies) . "\r\n";
        }
        $method = $this->getMethod();
        $uri = $this->getUrl(true);
        $protocol = $this->server->get('SERVER_PROTOCOL');

        return sprintf('%s %s %s', $method, $uri, $protocol) . "\r\n" . $this->headers . $cookieHeader . "\r\n" . $content;
    }

    /**
     * 初始化Request类的各类参数
     * @param array $query GET 参数
     * @param array $request POST 参数
     * @param array $attributes REQUEST attribute参数 即PATH_INFO 中解析出来的参数
     * @param array $cookies $_COOKIE 参数
     * @param array $files $_FILES 参数
     * @param array $server $_SERVER 参数
     * @param null $content raw body data 原始表单提交字符串
     */
    public function initialize(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null) {
        $this->query = new ParameterBag($query);
        $this->request = new ParameterBag($request);
        $this->attributes = new ParameterBag($attributes);
        $this->cookies = new ParameterBag($cookies);
        $this->files = new FileBag($files);
        $this->server = new ServerBag($server);
        $this->headers = new HeaderBag($this->server->getHeaders());
        $this->content = $content;

        $this->pathInfo = null;
        $this->method = null;
        $this->domain = null;
        $this->host = null;
        $this->port = null;
        $this->url = null;
        $this->baseUrl = null;
        $this->baseUrl = null;
        $this->baseUrlWithoutPath = null;
        $this->queryString = null;
        $this->format = null;
    }

    /**
     * 从全局变量中创建一个新的request请求
     * @return array|Request
     */
    public static function createFromGlobals() {
        $request = self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER);

        $contentType = $request->headers->get('CONTENT_TYPE');
        if (0 === strpos($contentType, 'application/x-www-form-urlencoded')) {
            parse_str($request->getContent(), $data);
            $request->request = new ParameterBag($data);
        }
        return $request;
    }

    /**
     * 复制一个request请求
     * @param array|null $query
     * @param array|null $request
     * @param array|null $attributes
     * @param array|null $cookies
     * @param array|null $files
     * @param array|null $server
     * @return Request
     */
    public function duplicate(array $query = null, array $request = null, array $attributes = null, array $cookies = null, array $files = null, array $server = null) {
        $duplicate = clone $this;
        if (!is_null($query)) {
            $duplicate->query = new ParameterBag($query);
        }
        if (!is_null($request)) {
            $duplicate->request = new ParameterBag($request);
        }
        if (!is_null($attributes)) {
            $duplicate->attributes = new ParameterBag($attributes);
        }
        if (!is_null($cookies)) {
            $duplicate->cookies = new ParameterBag($cookies);
        }
        if (!is_null($files)) {
            $duplicate->files = new FileBag($files);
        }
        if (!is_null($server)) {
            $duplicate->server = new ServerBag($server);
            $duplicate->headers = new HeaderBag($duplicate->server->getHeaders());
        }

        $duplicate->pathInfo = null;
        $duplicate->method = null;
        $duplicate->domain = null;
        $duplicate->host = null;
        $duplicate->port = null;
        $duplicate->url = null;
        $duplicate->baseUrl = null;
        $duplicate->baseUrl = null;
        $duplicate->baseUrlWithoutPath = null;
        $duplicate->queryString = null;
        $duplicate->format = null;


        return $duplicate;
    }

    /**
     * 获取原始请求数据文件流
     * @param bool $asResource
     * @return bool|false|resource|string|null
     */
    public function getContent($asResource = false) {
        $currentContentIsResource = is_resource($this->content);
        if (true === $asResource) {
            if ($currentContentIsResource) {
                //将指针设置为文件流的开头
                rewind($this->content);
                return $this->content;
            }
            if (is_string($this->content)) {
                $resource = fopen('php://temp', 'r+');
                fwrite($resource, $this->content);
                rewind($resource);
                return $resource;
            }

            $this->content = false;
            return fopen('php://input', 'rb');
        }

        if ($currentContentIsResource) {
            rewind($this->content);
            return stream_get_contents($this->content);
        }

        if (null === $this->content || false === $this->content) {
            $this->content = file_get_contents('php://input');
        }

        return $this->content;
    }

    /**
     * 利用工厂模式创建request类
     * @param array $query
     * @param array $request
     * @param array $attributes
     * @param array $cookies
     * @param array $files
     * @param array $server
     * @param null $content
     * @return array|Request
     */
    private static function createRequestFromFactory(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null) {
        if (!self::$requestFactory) {
            self::$requestFactory = new static($query, $request, $attributes, $cookies, $files, $server, $content);
        }
        return self::$requestFactory;
    }

    /**
     * 获取request请求的请求方式
     * @return string
     */
    public function getMethod() {
        if (!$this->method) {
            $this->method = strtoupper($this->server->get('REQUEST_METHOD', 'GET'));
        }
        return $this->method;
    }

    /**
     * 获取页面的content-type
     * @return int|string|null
     */
    public function getContentType() {
        return $this->getFormat($this->headers->get('CONTENT_TYPE'));
    }

    /**
     * 判断是否ajax请求
     * @return bool
     */
    public function isAjax() {
        return 'XMLHttpRequest' == $this->headers->get('X-Requested-With');
    }

    /**
     * 验证页面是否有缓存
     * @return bool
     */
    public function isNoCache() {
        return $this->headers->hasCacheControlDirective('no-cache') || 'no-cache' == $this->headers->get('Pragma');
    }

    /**
     * 获取当前域名，包括 协议、主机地址及端口号
     * @return string
     */
    public function getDomain() {
        if (is_null($this->domain)) {
            $scheme = $this->getScheme();
            $host = $this->getHost();
            $port = $this->getPort();
            if (('http' == $scheme && 80 == $port) || ('https' == $scheme && 443 == $port)) {
                $this->domain = $scheme . '://' . $host;
            } else {
                $this->domain = $scheme . '://' . $host . ':' . $port;
            }
        }
        return $this->domain;

    }

    /**
     * 获取url地址中得协议 即 http 或者 https
     * @return string
     */
    public function getScheme() {
        return $this->isSecure() ? 'https' : 'http';
    }

    /**
     * 获取请求的主机地址不带端口号
     * @return string|null
     */
    public function getHost() {
        if (is_null($this->host)) {
            $host = $this->server->get('HTTP_HOST');
            if ($this->isTrustedProxy()) {
                $trueHost = $this->getTrustedHeaderValue(self::HEADER_X_FORWARDED_HOST);
                $host = !empty($trueHost) ? $trueHost : $host;
            }

            $this->host = strtolower(preg_replace('/:\d+$/', '', trim($host)));
        }
        return $this->host;
    }

    /**
     * 获取请求的端口号
     * @return string|null
     */
    public function getPort() {
        if (is_null($this->port)) {
            if ($this->isTrustedProxy()) {
                $port = $this->getTrustedHeaderValue(self::HEADER_X_FORWARDED_PORT);
                if (!empty($port)) {
                    $this->port = $port;
                    return $this->port;
                }
            }
            $this->port = $this->server->get('SERVER_PORT');
        }
        return $this->port;
    }

    /**
     * 获取当前请求url地址,主要是处理path即query部分
     * url组成：  protocol://host[:port][/path][parameters][?query][#fragment]
     * protocol  协议，一般就是http或者https
     * host[:port] 访问的主机域名或Ip 及访问的端口号
     * path 网络资源在服务器的路径
     * parameters
     * query 查询字符串
     * fragment 片段，即锚点定位
     * @param bool $withDomain 是否带有域名信息
     * @return array|string|null
     */
    public function getUrl($withDomain = false) {
        if (is_null($this->url)) {
            $requestUri = $this->server->get('REQUEST_URI');
            //只保留path及query部分，去除fragment部分，但实际测试未发现会带入的情况
            if (false !== $pos = strpos($requestUri, '#')) {
                $requestUri = substr($requestUri, 0, $pos);
            }
            $this->url = $requestUri;
        }
        return $withDomain ? ($this->getDomain() . $this->url) : $this->url;
    }

    /**
     * 获取当前URL 不含QUERY_STRING
     * @access public
     * @param bool $withDomain 是否带有域名信息
     * @return string|$this
     */
    public function getBaseUrl($withDomain = false) {
        if (is_null($this->baseUrl)) {
            $url = $this->getUrl();
            $this->baseUrl = strpos($url, '?') ? strstr($url, '?', true) : $url;
        }
        return $withDomain ? ($this->getDomain() . $this->baseUrl) : $this->baseUrl;
    }

    /**
     * 获取不带pathinfo的url
     * @param $withDomain
     * @return mixed|null
     * @throws \Exception
     */
    public function getBaseUrlWithoutPathInfo($withDomain = false) {
        if (is_null($this->baseUrlWithoutPath)) {
            $this->baseUrlWithoutPath = $this->prepareBaseUrlWithoutPathInfo();
        }
        return $withDomain ? ($this->getDomain() . $this->baseUrlWithoutPath) : $this->baseUrlWithoutPath;
    }

    /**
     * 获取不带pathinfo信息及querystring的url
     * @param bool $withDomain
     * @return mixed|null
     * @throws \Exception
     */
    protected function prepareBaseUrlWithoutPathInfo() {
        $filename = basename($this->server->get('SCRIPT_FILENAME'));

        if (basename($this->server->get('SCRIPT_NAME')) === $filename) {
            $baseUrl = $this->server->get('SCRIPT_NAME');
        } elseif (basename($this->server->get('PHP_SELF')) === $filename) {
            $baseUrl = $this->server->get('PHP_SELF');
        } else {
            // Backtrack up the script_filename to find the portion matching
            // php_self
            $path = $this->server->get('PHP_SELF', '');
            $file = $this->server->get('SCRIPT_FILENAME', '');
            $segs = explode('/', trim($file, '/'));
            $segs = array_reverse($segs);
            $index = 0;
            $last = \count($segs);
            $baseUrl = '';
            do {
                $seg = $segs[$index];
                $baseUrl = '/' . $seg . $baseUrl;
                ++$index;
            } while ($last > $index && (false !== $pos = strpos($path, $baseUrl)) && 0 != $pos);
        }

        $url = $this->getUrl();
        if ('' !== $url && '/' !== $url[0]) {
            $url = '/' . $url;
        }
        if ($baseUrl && false !== $prefix = $this->getUrlencodedPrefix($url, $baseUrl)) {
            return $prefix;
        }
        if ($baseUrl && false !== $prefix = $this->getUrlencodedPrefix($url, rtrim(dirname($baseUrl), '/' . DIRECTORY_SEPARATOR) . '/')) {
            // directory portion of $baseUrl matches
            return rtrim($prefix, '/' . DIRECTORY_SEPARATOR);
        }

        $truncatedRequestUri = $url;
        if (false !== $pos = strpos($url, '?')) {
            $truncatedRequestUri = substr($url, 0, $pos);
        }

        $basename = basename($baseUrl);
        if (empty($basename) || !strpos(rawurldecode($truncatedRequestUri), $basename)) {
            // no match whatsoever; set it blank
            return '';
        }

        // If using mod_rewrite or ISAPI_Rewrite strip the script filename
        // out of baseUrl. $pos !== 0 makes sure it is not matching a value
        // from PATH_INFO or QUERY_STRING
        if (strlen($url) >= \strlen($baseUrl) && (false !== $pos = strpos($url, $baseUrl)) && 0 !== $pos) {
            $baseUrl = substr($url, 0, $pos + strlen($baseUrl));
        }

        return rtrim($baseUrl, '/' . DIRECTORY_SEPARATOR);
    }

    /**
     * 获取url前缀
     * @param string $string
     * @param string $prefix
     * @return bool|mixed
     */
    private function getUrlencodedPrefix(string $string, string $prefix) {
        if (0 !== strpos(rawurldecode($string), $prefix)) {
            return false;
        }

        $len = strlen($prefix);

        if (preg_match(sprintf('#^(%%[[:xdigit:]]{2}|.){%d}#', $len), $string, $match)) {
            return $match[0];
        }

        return false;
    }

    /**
     * 获取url中得querystring部分
     * @return array|string|null
     */
    public function getQueryString() {
        if (is_null($this->queryString)) {
            $queryString = $this->server->get('QUERY_STRING');
            if (empty($queryString)) {
                return null;
            }
            parse_str($queryString, $queryString);
            ksort($queryString);

            $this->queryString = http_build_query($queryString, '', '&', PHP_QUERY_RFC3986);
        }
        return $this->queryString;

    }

    /**获取请求pathinfo部分
     * @return array|bool|string|null
     * @throws \Exception
     */
    public function getPathInfo() {
        if (is_null($this->pathInfo)) {
            $this->pathInfo = $this->preparePathInfo();
        }
        return $this->pathInfo;
    }

    /**
     * 解析请求中pathinfo部分
     * @return array|bool|string|null
     * @throws \Exception
     */
    protected function preparePathInfo() {
        if (null === ($requestUri = $this->getUrl())) {
            return '/';
        }

        // Remove the query string from REQUEST_URI
        if (false !== $pos = strpos($requestUri, '?')) {
            $requestUri = substr($requestUri, 0, $pos);
        }
        if ('' !== $requestUri && '/' !== $requestUri[0]) {
            $requestUri = '/' . $requestUri;
        }

        if (null === ($baseUrl = $this->getBaseUrlWithoutPathInfo())) {
            return $requestUri;
        }

        $pathInfo = substr($requestUri, strlen($baseUrl));
        if (false === $pathInfo || '' === $pathInfo) {
            // If substr() returns false then PATH_INFO is set to an empty string
            return '/';
        }

        return (string)$pathInfo;
    }

    /**
     * 获取客户端真实IP
     * @return string
     */
    public function getClientIp() {
        //判断当前客户端IP是否在受信任的代理服务器列表中，则根据指定的header头信息获取到客户端IP
        if ($this->isTrustedProxy()) {
            $ip = $this->getTrustedHeaderValue(self::HEADER_X_NS_CLIENTIP);
            if (!empty($ip)) {
                return $ip;
            }
            $ip = $this->getTrustedHeaderValue(self::HEADER_X_FORWARDED_FOR);
            if (!empty($ip)) {
                return $ip;
            }
        }
        return $this->server->get('REMOTE_ADDR');
    }

    /**
     * 获取请求的端口号
     * @return string|null
     */
    public function getClientPort() {
        if ($this->isTrustedProxy()) {
            $port = $this->getTrustedHeaderValue(self::HEADER_X_NS_CLIENTPORT);
            if (!empty($port)) {
                $this->port = $port;
                return $this->port;
            }

            $port = $this->getTrustedHeaderValue(self::HEADER_X_FORWARDED_PORT);
            if (!empty($port)) {
                $this->port = $port;
                return $this->port;
            }
        }

        $this->port = $this->server->get('REMOTE_PORT');
        return $this->port;
    }

    /**
     * 判断是否安全访问,即是否https访问
     * @return bool
     */
    public function isSecure() {
        if ($this->isTrustedProxy()) {
            //如果来自受信任的透明代理，那么通过代理服务器传递过来的头信息进行判断
            $proto = $this->getTrustedHeaderValue(self::HEADER_X_FORWARDED_PROTO);
            return in_array(strtolower($proto), ['https', 'ssl'], true);
        }

        return (1 == $this->server->get('HTTPS') || 'on' == strtolower($this->server->get('HTTPS'))) ? true : false;
    }

    /**
     * 查看是否是受信任的代理服务器
     * @return bool
     */
    public function isTrustedProxy() {
        $ip = $this->server->get('REMOTE_ADDR');
        return in_array($ip, $this->trustedProxies);
    }

    /**
     * 获取受信任代理转发的头信息内容
     * @param $headerType   头信息名称
     * @return string|null
     */
    public function getTrustedHeaderValue($headerType) {
        if (!isset(self::$trustedHeaders[$headerType]) || empty(self::$trustedHeaders[$headerType])) {
            return null;
        }
        return $this->header()->get(self::$trustedHeaders[$headerType]);
    }

    /**
     * 设置代理服务器的信息
     * @param array $proxies
     * @param array $trustedHeaders 受信任的头信息名称
     */
    public function setHttpProxy($proxies = [], $trustedHeaders = []) {
        $this->trustedProxies = $proxies;
        self::$trustedHeaders = self::$trustedHeaders + $trustedHeaders;
    }

    /**
     * 获取当前请求的文档格式
     * @param string $default
     * @return string
     */
    public function getRequestFormat($default = 'html') {
        return is_null($this->format) ? $default : $this->format;
    }

    /**
     * 设置当前请求的文档格式
     * @param $format
     */
    public function setRequestFormat($format) {
        $this->format = $format;
    }

    /**
     * 获取请求的文档类型
     * @param $format
     * @return mixed|null
     */
    public function getMimeType($format) {
        return isset($this->formats[$format]) ? $this->formats[$format][0] : null;
    }

    /**
     * 获取请求的全部文档类型
     * @param $format
     * @return mixed|null
     */
    public function getMimeTypes($format) {
        return isset($this->formats[$format]) ? $this->formats[$format] : [];
    }

    /**
     * 根据文档类型获取文档的格式
     * @param $mimeType
     * @return int|string|null
     */
    public function getFormat($mimeType) {
        if (false !== $pos = strpos($mimeType, ';')) {
            $mimeType = trim(substr($mimeType, 0, $pos));
        }
        foreach ($this->formats as $format => $mimeTypes) {
            if (in_array($mimeType, $mimeTypes)) {
                return $format;
            }
        }
        return null;
    }

    /**
     * 设置文档格式
     * @param $format
     * @param $mimeType
     */
    public function setFormat($format, $mimeType) {
        $this->formats[$format] = is_array($mimeType) ? $mimeType : [$mimeType];
    }

    /**
     * 判断是否期望返回json格式数据
     * @return bool
     */
    public function expectsJson() {
        return $this->isAjax() || $this->wantsJson();
    }

    /**
     * 判断是否期望返回json格式数据
     * @return bool
     */
    public function wantsJson() {
        $acceptable = $this->headers->get('accept', '');
        return !empty($acceptable) && Str::contains($acceptable, ['/json', '+json']);
    }

}